#include <stdio.h>
#include <stdint.h>

// comment out the following macro if you want debugging output
//#define OP_OS(...)
#define OPALLOC_IMPLEMENTATION
#include "opalloc.h"

static void prefix_function(void);
static void postfix_function(void);

#define TEST_INIT prefix_function()
#define TEST_FINI postfix_function()
#include "acutest.h"

#ifndef UNUSED
#define UNUSED(X) ((void)X)
#endif

#define MINIMUM_ALLOCATION_COUNT 4

static void my_error_handler(const char *f, const int l, const char *m)
{
    OP_OS("\nError in %s(%d): %s\n", f, l, m);
}

typedef struct test_object
{
    bool running;
    int stack_size;
} test_object;

static void prefix_function()
{
    op_error_handler = my_error_handler;
}

static void postfix_function()
{
}

#define validate_stats(A, O, M, C) do {                                               \
    op_allocator_stats S;                                                             \
    S = op_ll_get_allocator_stats(A);                                                 \
    TEST_CHECK(S.object_size == O);                                                   \
    TEST_MSG("object_size: expected %ld, got %ld", (size_t)O, S.object_size);         \
    TEST_CHECK(S.maximum_objects == M);                                               \
    TEST_MSG("maximum_objects: expected %ld, got %ld", (size_t)M, S.maximum_objects); \
    TEST_CHECK(S.active_objects == C);                                                \
    TEST_MSG("active_objects: expected %ld, got %ld", (size_t)C, S.active_objects);   \
} while (false)

static void test_initialize_deinitialize(void)
{
    for (size_t size = MINIMUM_ALLOCATION_COUNT; size < 1000; size += 17)
    {
        op_allocator allocator1 = op_ll_initialize_allocator(sizeof(test_object), size, OP_DOUBLING_INDIVIDUAL);
        TEST_CHECK(allocator1);
        validate_stats(allocator1, sizeof(test_object), size, 0);

        op_allocator allocator2 = op_ll_initialize_allocator(sizeof(test_object), size, OP_DOUBLING_CHUNK);
        TEST_CHECK(allocator2);
        validate_stats(allocator2, sizeof(test_object), size, 0);

        op_allocator allocator3 = op_ll_initialize_allocator(sizeof(test_object), size, OP_LINEAR_INDIVIDUAL);
        TEST_CHECK(allocator3);
        validate_stats(allocator3, sizeof(test_object), size, 0);

        op_allocator allocator4 = op_ll_initialize_allocator(sizeof(test_object), size, OP_LINEAR_CHUNK);
        TEST_CHECK(allocator4);
        validate_stats(allocator4, sizeof(test_object), size, 0);

        op_ll_deinitialize_allocator(allocator1);
        op_ll_deinitialize_allocator(allocator2);
        op_ll_deinitialize_allocator(allocator3);
        op_ll_deinitialize_allocator(allocator4);
    }
}

void test_item_allocation(void)
{
    op_allocator allocator = op_ll_initialize_allocator(sizeof(test_object), MINIMUM_ALLOCATION_COUNT, OP_DOUBLING_INDIVIDUAL);
    TEST_CHECK(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 0);

    test_object *item1a = op_ll_allocate_object(allocator);
    TEST_CHECK(item1a);
    TEST_CHECK(!item1a->running);
    TEST_CHECK(item1a->stack_size == 0);

    test_object *item1b = op_ll_allocate_object(allocator);
    TEST_CHECK(item1b);
    TEST_CHECK(item1a != item1b);
    TEST_CHECK(!item1b->running);
    TEST_CHECK(item1b->stack_size == 0);

    test_object *item1c = op_ll_allocate_object(allocator);
    TEST_CHECK(item1c);
    TEST_CHECK(item1c != item1b);
    TEST_CHECK(item1c != item1a);
    TEST_CHECK(!item1c->running);
    TEST_CHECK(item1c->stack_size == 0);

    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 3);

    op_ll_deinitialize_allocator(allocator);
}

void test_item_doubling(void)
{
    test_object *item;
    op_allocator allocator = op_ll_initialize_allocator(sizeof(test_object), MINIMUM_ALLOCATION_COUNT, OP_DOUBLING_INDIVIDUAL);
    TEST_CHECK(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 0);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
        validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, i);
    }

    item = op_ll_allocate_object(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 2, MINIMUM_ALLOCATION_COUNT + 1);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
    }

    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 4, MINIMUM_ALLOCATION_COUNT * 2 + 1);

    op_ll_deinitialize_allocator(allocator);

    UNUSED(item);
}

void test_item_deletion(void)
{
    test_object *item;
    op_allocator allocator = op_ll_initialize_allocator(sizeof(test_object), MINIMUM_ALLOCATION_COUNT, OP_DOUBLING_INDIVIDUAL);
    TEST_CHECK(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 0);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
        if (i == 2)
        {
            op_ll_deallocate_object(allocator, item);
        }
    }

    item = op_ll_allocate_object(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, MINIMUM_ALLOCATION_COUNT);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
    }

    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 2, MINIMUM_ALLOCATION_COUNT * 2);

    op_ll_deinitialize_allocator(allocator);
}

void test_item_linear(void)
{
    test_object *item;
    op_allocator allocator = op_ll_initialize_allocator(sizeof(test_object), MINIMUM_ALLOCATION_COUNT, OP_LINEAR_INDIVIDUAL);
    TEST_CHECK(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 0);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
        validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, i);
    }

    item = op_ll_allocate_object(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 2, MINIMUM_ALLOCATION_COUNT + 1);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
    }

    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 3, MINIMUM_ALLOCATION_COUNT * 2 + 1);

    op_ll_deinitialize_allocator(allocator);

    UNUSED(item);
}

void test_item_doubling_chunk(void)
{
    test_object *item;
    op_allocator allocator = op_ll_initialize_allocator(sizeof(test_object), MINIMUM_ALLOCATION_COUNT, OP_DOUBLING_CHUNK);
    TEST_CHECK(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 0);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
        validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, i);
    }

    item = op_ll_allocate_object(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 2, MINIMUM_ALLOCATION_COUNT + 1);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
    }

    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 4, MINIMUM_ALLOCATION_COUNT * 2 + 1);

    op_ll_deinitialize_allocator(allocator);

    UNUSED(item);
}

void test_item_linear_chunk(void)
{
    test_object *item;
    op_allocator allocator = op_ll_initialize_allocator(sizeof(test_object), MINIMUM_ALLOCATION_COUNT, OP_LINEAR_CHUNK);
    TEST_CHECK(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, 0);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
        validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, i);
    }

    item = op_ll_allocate_object(allocator);
    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 2, MINIMUM_ALLOCATION_COUNT + 1);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = op_ll_allocate_object(allocator);
    }

    validate_stats(allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 3, MINIMUM_ALLOCATION_COUNT * 2 + 1);

    op_ll_deinitialize_allocator(allocator);

    UNUSED(item);
}

/* declare a type-safe allocator suite */
OP_HL_DECLARE_ALLOCATOR(test_object, MINIMUM_ALLOCATION_COUNT, OP_DOUBLING_CHUNK);

void test_allocator_macro(void)
{
    test_object *item;

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = allocate_test_object();
        validate_stats(test_object_allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT, i);
    }

    item = allocate_test_object();
    validate_stats(test_object_allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 2, MINIMUM_ALLOCATION_COUNT + 1);

    for (int i = 1; i <= MINIMUM_ALLOCATION_COUNT; i++)
    {
        item = allocate_test_object();
    }

    validate_stats(test_object_allocator, sizeof(test_object), MINIMUM_ALLOCATION_COUNT * 4, MINIMUM_ALLOCATION_COUNT * 2 + 1);

    deinitialize_test_object_allocator();

    UNUSED(item);
}

TEST_LIST =
{
    { "initialize and deinitialize", test_initialize_deinitialize },
    { "item allocation and cleanup", test_item_allocation         },
    { "item doubling growth",        test_item_doubling           },
    { "item deletion",               test_item_deletion           },
    { "item linear growth",          test_item_linear             },
    { "item doubling chunk growth",  test_item_doubling_chunk     },
    { "item linear chunk growth",    test_item_linear_chunk       },
    { "type-safe allocator macro",   test_allocator_macro         },

    { NULL, NULL },
};
